from threading import Thread
import serial
import time
import sys

##### helper functions ######
def addr2list(addr, numbytes=8):
  '''Convert a numeric address into a list of bytes.'''
  return [(addr>>i*8)&0xff for i in range(numbytes-1,0-1,-1)]

def n2add16(n):
  '''Convert numeric data to 16-bit address.'''
  return addr2list(n, 2)

def n2add64(n):
  '''Convert numeric data to 64-bit address.'''
  return addr2list(n, 8)

def a2list(str):
  '''Convert a string of data into a list of bytes.'''
  return [ord(c) for c in str]

def addr2str(list, prefix="", delimiter=":"):
  '''Convert an address (list of values) into a human-readable string.'''
  return prefix + delimiter.join(["%02X" % x for x in list])

def l2s(list):
  '''Convert a list of char numbers to a string of text.'''
  return ''.join([chr(c) for c in list])

#### main XBEE API class ####

class XbeeDestination:
  def __init__(self, addr64bit=0, addr16bit=0xfffe, nodeid=""):
    self.add64 = addr64bit
    self.add16 = addr16bit
    self.nodeid = nodeid
    #self.type = "coordinator"

  @property
  def add64():
    '''Module's 64 bit address, in decimal form'''
    def fset(self,newaddress):
      self.add64 = newaddress

    def fget(self):
      return n2add64(self.add64)
  
  @property
  def add16():
    '''Module's 16 bit address, in decimal form'''
    def fset(self,newaddress):
      self.add16 = newaddress

    def fget(self):
      return n2add16(self.add16)



class XbeeApi(Thread):
  def __init__(self, port='/dev/ttyUSB0', baud=38400):
    Thread.__init__(self)
    self.ser = serial.Serial(port, baud)
    self.ser.flushInput()
    self.ser.flushOutput()
    self.framecount = 0
    self.start()
    self.devices = {}
    self.packet_types = {
         # Explicit RX
         0x91:[
           {'name':'mac',       'len':8, 'conv':None,'postconv':None},
           {'name':'addr',      'len':2, 'conv':None,'postconv':None},
           {'name':'s_endpt',   'len':1, 'conv':None,'postconv':None},
           {'name':'d_endpt',   'len':1, 'conv':None,'postconv':None},
           {'name':'clusterid', 'len':2, 'conv':None,'postconv':None},
           {'name':'profileid', 'len':2, 'conv':None,'postconv':None},
           {'name':'options',   'len':1, 'conv':None,'postconv':None},
           {'name':'data',    'len':255, 'conv':chr,'postconv':("").join}
           ],

         # TX Status
         0x8B:[
           {'name':'frameid',   'len':1, 'conv':None,'postconv':None},
           {'name':'addr',      'len':2, 'conv':None,'postconv':None},
           {'name':'retry_cnt', 'len':1, 'conv':None,'postconv':None},
           {'name':'status',    'len':1, 'conv':None,'postconv':None},
           {'name':'discovery', 'len':1, 'conv':None,'postconv':None}
           ],
         #incoming message
         0x90:[
           {"name":"mac",     "len":8,"conv":None,"postconv":None},
           {"name":"address", "len":2,"conv":None,"postconv":None},
           {"name":"options", "len":1,"conv":None,"postconv":None},
           {"name":"data",    "len":255,"conv":chr,"postconv":("").join}
           ],
          #AT reply
         0x88:[
           {"name":"id",    "len":1,"conv":None,"postconv":None},
           {"name":"cmd",   "len":2,"conv":chr,"postconv":("").join},
           {"name":"status","len":1,"conv":None,"postconv":None},
           {"name":"data",  "len":255,"conv":None,"postconv":None}
           ],
         #remote AT reply
         0x97:[
           {"name":"id",      "len":1,"conv":None,"postconv":None},
           {"name":"mac",     "len":8,"conv":None,"postconv":None},
           {"name":"address", "len":2,"conv":None,"postconv":None},
           {"name":"cmd",     "len":2,"conv":chr,"postconv":("").join},
           {"name":"status",  "len":1,"conv":None,"postconv":None},
           {"name":"data",  "len":255,"conv":None,"postconv":None}
           ]


        }
    self.callbacks = {
        'data_received':self.on_rcv_data,
        'explicit_rx':self.on_explicit_rx,
        'standard_rx':self.on_incoming_data,
        'status_msg':self.on_status_message,
        'cmd_answer':self.decode_cmd
    }

  def make_frame_count(self):
    '''Generate unique frame identifier, max 255'''
    self.framecount %= 255
    self.framecount += 1
    return self.framecount

  def checksum(self,packet):
    '''Calculate checksum of a data packet
    Format : packet is a list, does not contain 3 start bytes'''
    return 0xFF-(sum(packet)&0xFF)

  def pack_and_send(self, frame):
    '''Add delimiters and checksum to a data frame
    Data frame must already contain commands and addresses'''
    pack = [0x7E, 0x00, len(frame)] 
    pack.extend(frame)
    pack.append(self.checksum(frame))
    # packet is now ready to be transmitted
    #outstr = ''.join([chr(c) for c in pack])
    outstr = l2s(pack)
    self.ser.write(outstr)

  def send_text(self, dest, text):
    '''Send a text message to a destination node.'''
    command = 0x10
    frameid = self.make_frame_count()
    radius, options = 0, 0
    # make up packet
    p = [command, frameid] + n2add64(dest.add64) + n2add16(dest.add16) + [radius, options]
    p.extend(a2list(text))
    self.pack_and_send(p)
    return frameid

  def send_command(self, cmd, params=None):
    '''Send an AT command to local module.
    Params, if given, must be a list.'''
    command = 0x08
    frameid = self.make_frame_count()
    p = [command, frameid] + a2list(cmd)
    if params:
      p.extend(params)
    self.pack_and_send(p)
    return frameid
    
  def send_remote_command(self, dest, cmd, params=None, apply=0x02):
    '''Send an AT command to remote module.
    if apply set to 0, module will wait for CN to apply changes.
      Params, if given, must be a list.'''
    command = 0x17
    frameid = self.make_frame_count()
    p = [command, frameid] + n2add64(dest.add64) + n2add16(dest.add16) + [apply]
    p.extend(a2list(cmd))
    if params:
      p.extend(params)
    self.pack_and_send(p)
    return frameid


  def run(self):
    while 1:
      rcv = self.ser.read()
      #print hex(ord(rcv))
      if ord(rcv) == 0x7e: # found start delimiter
        pack = self.unpack()
        if pack:
          self.on_rcv_data(pack)
      time.sleep(0.0001)

  def unpack(self):
    #print "unpacking"
    len_msb = ord(self.ser.read())
    len_lsb = ord(self.ser.read())
    p_len = len_lsb + (len_msb<<8)
    code = ord(self.ser.read())
    #print "MSB : 0x%02x  LSB : 0x%02x  Code : 0x%02x" % (len_msb, len_lsb, code)
    rx_buffer = []
    #print "max iter = %d" % max
    for i in range(p_len):
      rx_buffer.append(ord(self.ser.read()))

    chksum = (sum(rx_buffer) + code)&0xFF
    if not ((chksum - 0xFF) == 0):
      return None

    try:
      packet_fields = self.packet_types[code]
    except:
      print "ERROR : Packet 0x%02X not recognized!" % code
      return None
    else:
      pack = {}
      pack['code'] = code
      pack['data'] = {}
      buff_index = 0
      for field in packet_fields:
        values = []
        for k in range(field['len']):
          if buff_index < (len(rx_buffer)-1):
            val = rx_buffer[buff_index]
            if field['conv']:
              val = field['conv'](val)
            values.append(val)
            buff_index += 1
          else:
            break
        if field['postconv']:
          values=field['postconv'](values)
        pack['data'][field['name']]=values
    return pack

  def decode_cmd(self, pack):
    data = pack['data']
    if data['cmd'] == 'ND': # node discover
      nd_data = data['data']
      nd = {}
      nd['add'] = nd_data[0:1]
      nd['mac'] = nd_data[2:10]
      try:
        i = nd_data.index(0, 10)
      except:
        print "Error : couldn't find node ID string"
      else:
        nd['name'] = l2s(nd_data[10:i])
        nd['parent_net'] = nd_data[i:i+2]; i+=2
        nd['dev_type'] = nd_data[i:i+1]; i+=1
        nd['status'] = nd_data[i:i+1]; i+=1
        nd['profile_id'] = nd_data[i:i+2]; i+=2
        nd['manuf_id'] = nd_data[i:i+2]; i+= 2
        self.devices[nd['name']] = nd
        print "---- device table ----"
        for device_key in self.devices:
          print "%s -> %s" % (device_key,addr2str(self.devices[device_key]['mac']))

  def on_status_message(self, pack):
    rx_data = pack['data']
    dest = (rx_data['addr'][0]<<8) + rx_data['addr'][1]
    discovery_opts = ['None', 'Addr', 'Route', 'Addr+Route']

    if rx_data['discovery'][0] < len(discovery_opts):
      discovery = discovery_opts[rx_data['discovery'][0]]
    else:
      discovery = 'N/A'

    status = "SUCCESS" if rx_data['status'][0] == 0 else "FAILURE"
    print "%s sending frame (0x%02x) to (0x%04x) -- retry count %d, discovery : %s, status [%02x]" % \
        (status, rx_data['frameid'][0], dest, rx_data['retry_cnt'][0], discovery, rx_data['status'][0])

  def on_incoming_data(self, pack):
    print "\n\r--------- INCOMING PACKET --------"
    print "| Code : 0x%02X                    |" % pack['code']
    print "| Mac  : %-23s |" % addr2str(pack['data']['mac'])
    print "| Opts : %-23s |" % ("Acknowledged" if pack['data']['options'] == 1 else "Broadcast")
    print "| Addr : %-23s |" % addr2str(pack['data']['address'])
    print "----------------------------------"
    print pack['data']['data']
    print "----------------------------------\n\r"

  def on_explicit_rx(self, pack):
    print "\n\r--------- INCOMING PACKET --------"
    print "| Code : 0x%02X                    |" % pack['code']
    print "| Mac  : %-23s |" % addr2str(pack['data']['mac'])
    print "| Addr : %-23s |" % addr2str(pack['data']['addr'])
    #print "Message >> %s" % rx_data['data']
    print "----------------------------------"
    print pack['data']['data']
    print "----------------------------------\n\r"



  def on_rcv_data(self, pack):
    rx_data = pack['data']
    #print "------------------------------"
    #print "------------------------------"
    if(pack['code'] == 0x91):
      self.on_explicit_rx(pack)

    elif pack['code'] == 0x8B:
      self.on_status_message(pack)

    elif pack['code'] == 0x90:
      self.on_incoming_data(pack)

    elif pack['code'] == 0x88 or pack['code'] == 0x97:
      self.decode_cmd(pack)

    else:
      print "Code is 0x%X" % pack['code']
      for field in rx_data:
        print field
        print rx_data[field]
        if field == 'data':
          print ('').join([chr(c) for c in rx_data[field]])
          print (' ').join([hex(c) for c in rx_data[field]])


  

if __name__ == '__main__':
  from random import randint
  import os

  hostname = os.uname()[1]
  ftdiports = {'einherjer':'/dev/ttyUSB0', 'loki':'/dev/ttyUSB0'}
  
  if ftdiports.has_key(hostname):
    xbeeport = ftdiports[hostname]
  else:
    xbeeport = '/dev/ttyUSB0'
  xbee = XbeeApi(port=xbeeport)

  coord = XbeeDestination(nodeid="GIZMO0")
  gizmo1 = XbeeDestination(addr64bit=0x0013a20040341D05, nodeid="GIZMO1")
  gizmo2 = XbeeDestination(addr64bit=0x0013a20040341D07, nodeid="GIZMO2")
  #gizmo3 = XbeeDestination(addr64bit=0x0013a20040341D06, nodeid="GIZMO0")
  print "Press enter to send packets again -- type 'q' and ENTER to quit\r\n"

  while(1):
    id_tuple = (hostname, randint(1,100))
    #xbee.send_text(coord, "\r\nHello coordinator from the API on %s #%d\r\n" % id_tuple)
    #time.sleep(0.3)
    #xbee.send_text(gizmo1, "\r\nCalling GIZMO1 from %s (#%d)\r\n" % id_tuple)
    time.sleep(0.1)
    #xbee.send_text(gizmo2, "\r\nCalling GIZMO2 from %s (#%d)\r\n" % id_tuple)
    time.sleep(0.1)
    #xbee.send_text(gizmo07, "\r\nCalling GIZMO07 from %s (#%d)\r\n" % id_tuple)

    ## RGB Colour test, sample packets
    # set baudrate to 9600
    id = xbee.send_remote_command(gizmo2, 'BD', [0x03])
    time.sleep(1)

    for red in range(100):
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,red,0,0]]))
      time.sleep(0.07)
    for blue in range(100):
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,255,0,blue]]))
      time.sleep(0.07)


    for i in range(30):
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,0,0,255]]))
      time.sleep(1.1)
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,0,255,0]]))
      time.sleep(1.1)
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,255,0,0]]))
      time.sleep(1.1)
      xbee.send_text(g255, ('').join([chr(x) for x in [0x6E,255,0,255]]))
      time.sleep(1.1)
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,255,255,0]]))
      time.sleep(1.1)
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,0,255,255]]))
      time.sleep(1.1)
      xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,255,255,255]]))
      time.sleep(1.1)

    xbee.send_text(gizmo2, ('').join([chr(x) for x in [0x6E,0,0,0]]))

    #time.sleep(0.1)
    #xbee.send_command("NI")
    #xbee.send_command("BD")
    #xbee.send_command("ND")
    time.sleep(0.1)
    #xbee.send_command("DN",a2list("GIZMO2"))
    #xbee.send_remote_command(coord, 'NI')
    #id = xbee.send_remote_command(coord, 'ND')
    #print "Sending command with id %02x" % id
    #time.sleep(3)
    #id = xbee.send_command('ND')
    #print "Sending command with id %02x" % id
    #id = xbee.send_remote_command(gizmo07, 'LT', [0x00])
    

    time.sleep(1)
    s = raw_input()
    if s == 'q':
      sys.exit()

